test(platform-wallet): e2e framework + scenario suite, upstream bug pins + setup-gate hardening (v54: 98 PASS/10 FAIL)#3549
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAn end-to-end testing framework for Changes
Sequence Diagram(s)sequenceDiagram
participant Test as E2E Test
participant Harness as E2eContext Harness
participant Registry as Wallet Registry
participant Bank as BankWallet
participant TWallet as TestWallet
participant Manager as PlatformWalletManager
participant SDK as SDK/PlatformWallet
participant Cleanup as Cleanup
Test->>Harness: init() first call
Harness->>Registry: open(test_wallets.json)
Harness->>Cleanup: sweep_orphans()
Cleanup->>Registry: list_orphans()
Cleanup->>Manager: create from orphan seed
Cleanup->>SDK: sync & drain to bank
Cleanup->>Registry: remove_orphan_entry
Harness->>Bank: load from mnemonic
Harness->>Bank: sync_balances()
Harness->>Bank: fund_address(test_addr1, credits)
Harness->>SDK: transfer via bank wallet
Test->>Test: setup() generates seed
Test->>Manager: create TestWallet
Test->>TWallet: create(seed)
Test->>TWallet: next_unused_address() → addr2
Test->>Bank: fund_address(addr2, TRANSFER_CREDITS)
Test->>SDK: transfer via bank
Test->>TWallet: wait_for_balance(addr2, expected)
TWallet->>SDK: sync_balances()
Test->>SDK: transfer(addr2 → addr1, TRANSFER_CREDITS)
SDK->>SDK: execute, compute fee
Test->>TWallet: verify balances & fee
Test->>Test: teardown()
Test->>Cleanup: teardown_one(test_wallet)
Cleanup->>TWallet: drain all addresses to bank
Cleanup->>Registry: remove_entry
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds an end-to-end (wallet → SDK → broadcast) integration test harness to rs-platform-wallet and introduces the first live test case (address-funds transfer), alongside a production fix to InputSelection::Auto input selection so generated transitions satisfy protocol structure rules.
Changes:
- Added a reusable E2E framework under
packages/rs-platform-wallet/tests/e2e/(workdir slot locking, bank wallet, persistent registry, cleanup/sweep, wait hub, signer, SDK wiring). - Added the first E2E test case: transferring credits between two platform-payment addresses in a test wallet (ignored by default).
- Fixed
auto_select_inputsin production code to avoid selecting full balances as “input credits”, and added unit tests for the selection logic.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs | Fixes auto input selection; adds pure helper + unit tests for selection behavior. |
| packages/rs-platform-wallet/tests/e2e.rs | Adds the integration test crate root and module wiring for the e2e suite. |
| packages/rs-platform-wallet/tests/e2e/README.md | Operator/setup documentation for running live e2e tests. |
| packages/rs-platform-wallet/tests/e2e/cases/mod.rs | Declares e2e test modules. |
| packages/rs-platform-wallet/tests/e2e/cases/transfer.rs | First e2e test exercising funding + self-transfer + teardown. |
| packages/rs-platform-wallet/tests/e2e/framework/mod.rs | Framework public surface (setup, errors, prelude) and module layout. |
| packages/rs-platform-wallet/tests/e2e/framework/harness.rs | E2eContext singleton init: config, workdir locking, SDK, manager, bank, registry, startup sweep. |
| packages/rs-platform-wallet/tests/e2e/framework/config.rs | Env/.env configuration loader for the harness. |
| packages/rs-platform-wallet/tests/e2e/framework/sdk.rs | Constructs dash_sdk::Sdk with TrustedHttpContextProvider and DAPI address resolution. |
| packages/rs-platform-wallet/tests/e2e/framework/workdir.rs | Cross-process workdir slot selection via flock. |
| packages/rs-platform-wallet/tests/e2e/framework/panic_hook.rs | Installs panic hook to cancel background work on panic. |
| packages/rs-platform-wallet/tests/e2e/framework/wait_hub.rs | Notify-based hub bridging wallet/SPV/platform events to async waiters. |
| packages/rs-platform-wallet/tests/e2e/framework/wait.rs | Async waiting helpers (event-driven balance wait + generic polling). |
| packages/rs-platform-wallet/tests/e2e/framework/signer.rs | Seed-backed Signer<PlatformAddress> with eager DIP-17 key cache. |
| packages/rs-platform-wallet/tests/e2e/framework/wallet_factory.rs | Test wallet factory + SetupGuard (panic-safe registry-backed lifecycle). |
| packages/rs-platform-wallet/tests/e2e/framework/registry.rs | JSON-backed persistent registry for panic-safe orphan recovery. |
| packages/rs-platform-wallet/tests/e2e/framework/cleanup.rs | Startup sweep + per-test teardown draining funds back to bank. |
| packages/rs-platform-wallet/tests/e2e/framework/bank.rs | Loads and syncs a pre-funded bank wallet; serialized funding API. |
| packages/rs-platform-wallet/tests/e2e/framework/context_provider.rs | Retained (disabled) SPV-backed SDK context provider module for future re-enable. |
| packages/rs-platform-wallet/tests/e2e/framework/spv.rs | Retained (disabled) SPV startup/readiness helpers for future re-enable. |
| packages/rs-platform-wallet/Cargo.toml | Adds dev-dependencies needed by the e2e harness. |
| Cargo.lock | Locks new/updated dependencies for the added test tooling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… transfer test Test runs on the default tokio_shared_rt(shared) runtime without forcing multi_thread flavor. Confirms the harness works under single-threaded scenarios. Resolves PR #3549 thread r-DD2o. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arget CodeRabbit caught a critical bug on PR #3554's `select_inputs`: the helper ensured `Σ inputs.credits == Σ outputs.credits` (the protocol's structural invariant) but did NOT ensure that the address targeted by `DeductFromInput(0)` had post-consumption remaining balance >= the estimated fee. Worked example from CodeRabbit: candidates = [(addr_a, 20M), (addr_b, 50M)] // addr_a < addr_b lex total_output = 30M fee_strategy = [DeductFromInput(0)] Old result = {addr_a: 20M, addr_b: 10M} // Σ matches; addr_a drained Drive applies DeductFromInput(0) over inputs sorted by key (BTreeMap order), hitting addr_a — whose remaining balance is 0 — so `min(fee, 0) = 0`, `fee_fully_covered = false`, validator rejects with AddressesNotEnoughFundsError. The Wave-8 single-input live e2e accidentally avoided this because the fee target had ~1B credits left over after consumption — multi-input auto-selected transfers would have hit it on first contact. This rewrite: - Phase 1 (unchanged): pick smallest DIP-17-ordered prefix covering total_output + estimated_fee. - Phase 2: identify the fee target = lex-smallest address in the prefix (= `BTreeMap` index 0, what `DeductFromInput(0)` will hit per `rs-dpp/src/address_funds/fee_strategy/.../v0/mod.rs`). - Phase 3: consume the *minimum* allowed amount from the fee target (`max(min_input_amount, total_output − Σ other balances)`) so it retains the most remaining balance for fee deduction. Error out with a descriptive AddressOperation if even that minimum leaves less than `estimated_fee` remaining. - Phase 4: distribute the rest of `total_output` across the other prefix entries in DIP-17 order. - Phase 5: defensive invariant checks. `min_input_amount` is fetched from `platform_version.dpp.state_transitions.address_funds.min_input_amount` (currently 100k across v1/v2/v3 of platform-version). For non-`[DeductFromInput(0)]` fee strategies the helper falls back to the previous "consume from front" distribution that only enforces the Σ invariant — none of the wallet's call sites use anything else today. Tests: - updated `two_input_selection_trims_only_the_last` → `two_input_selection_keeps_fee_headroom_at_index_zero` to assert the new distribution AND the headroom invariant. - updated `fee_only_tail_input_does_not_inflate_input_sum`'s expected outputs (the tail is no longer dropped — it absorbs the consumption the fee target sheds). - added `fee_target_keeps_remaining_for_fee_deduction` (CodeRabbit's exact scenario, with the headroom invariant as the load-bearing assertion). - added `fee_headroom_violation_errors` (lex-smallest address too small to retain headroom → descriptive error rather than transition the validator will reject). - `single_input_oversized_balance_trims_to_output_amount`, `insufficient_balance_errors`, `no_candidates_errors` pass unchanged. `cargo test -p platform-wallet --lib` → 117 / 117 green `cargo clippy -p platform-wallet --tests -- -D warnings` → clean `cargo fmt -p platform-wallet --check` → clean Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ee-headroom bug Adds `pre_fix_buggy_selector_output_is_rejected_by_protocol_fee_deduction` to the `select_inputs` test module. Reconstructs the exact `inputs` map the pre-fix `auto_select_inputs` would have returned for CodeRabbit's example (candidates (20M, 50M), total_output 30M, `DeductFromInput(0)`), runs the post-consumption remaining balances through the live dpp fee-deduction code path, and asserts `fee_fully_covered == false` — i.e. the protocol rejects it with `AddressesNotEnoughFundsError`. Distinct from `fee_target_keeps_remaining_for_fee_deduction`, which asserts the new selector's output meets the headroom invariant. This reproduction proves the bug at the protocol layer rather than merely asserting "the new output looks different" — it would have stayed red without the fix in 9ea9e70. Verification: - cargo check --tests -p platform-wallet OK - cargo clippy --tests -p platform-wallet -- -D warnings OK - cargo fmt -p platform-wallet OK - cargo test -p platform-wallet --lib 118/118 Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
…descending Internal-only change to `auto_select_inputs`. Candidates were previously collected in DIP-17 derivation index order; now they sort by balance descending before being handed to `select_inputs`. Mirrors the dash-evo-tool allocator (`src/ui/wallets/send_screen.rs:155-157`). Effects: - Single largest balance covering `total_output + estimated_fee` => 1-input result, no multi-input case, no lex-smallest fee headroom logic firing. Common path simplified. - Multi-input cases (when the largest alone isn't enough) still go through the headroom-respecting distribution introduced in 9ea9e70 — unchanged, still correct. - No public API change. `transfer()`, `auto_select_inputs`, `select_inputs` signatures all identical. Adds `descending_order_picks_single_largest_when_sufficient` to the existing test module to lock in the common-path behavior. Other tests pass candidates directly to `select_inputs` and are order-agnostic by design — unchanged. The `fee_headroom_violation_errors` error message now includes the fee-target address, its balance, required headroom, and remaining-after-consumption to ease debugging. Verification: - cargo check --tests -p platform-wallet OK - cargo clippy --tests -p platform-wallet -- -D warnings OK - cargo fmt -p platform-wallet OK - cargo test -p platform-wallet --lib 119/119 Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
…egy, retry on Phase 3 fail Addresses the second wave of review findings on PR #3554: 1. [BLOCKING] Phase 4 distribution no longer produces inputs below `min_input_amount`. `auto_select_inputs` now filters candidates with `balance < min_input_amount` upfront — they cannot legally appear in the inputs map. In Phase 4, when a non-fee-target tail entry would consume less than `min_input_amount`, the residue rolls back into the fee target's consumption (which has surplus headroom by construction). Returns a descriptive error if rollback would violate the fee-target headroom invariant. 2. [BLOCKING] `transfer()` rejects unsupported `fee_strategy` shapes for `InputSelection::Auto`. Auto-select currently only implements protocol-correct logic for `[DeductFromInput(0)]`; any other strategy returns `PlatformWalletError::AddressOperation` with a clear message redirecting callers to `InputSelection::Explicit`. Explicit paths still accept arbitrary strategies (caller's responsibility). 3. [BLOCKING] When Phase 3 (`fee_target_min > fee_target_max`) fails in `select_inputs`, the algorithm now extends the prefix with the next candidate and retries instead of erroring out. Larger prefixes may yield a different lex-smallest fee target with sufficient headroom. Errors out only when candidates are exhausted and no covering prefix is feasible. 4. [SUGGESTION] `select_inputs` returns an early descriptive error when `total_output < min_input_amount` — the protocol forbids this regardless of input shape, so an explicit error beats the internal "should never trip" branch that some callers were reaching. 5. [SUGGESTION] Existing selector tests now also build a minimal `AddressFundsTransferTransitionV0` and run `validate_structure`, asserting protocol-level validity in addition to the `Σ inputs == total_output` invariant. Catches future regressions without needing a live node. Coderabbit findings DUuz (#3554), DUu1 (#3554), E5L5 (#3554), thepastaclaw findings F9fo, GMHz, GMH5, GMH_, F9fv addressed. Outdated F9fk references the renamed test from before 9ea9e70. Nitpicks F9fz/GMID/F9f5/GMIH deferred (unreachable / low value). Verification: - cargo check --tests -p platform-wallet OK - cargo clippy --tests -p platform-wallet -- -D warnings OK - cargo fmt -p platform-wallet OK - cargo test -p platform-wallet --lib 121/121 Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
… work Apply claudius:coding-best-practices rules: length cap (<=2 preferred, 3 mediocre), present-state only (no Wave/PR-number history), two-tier (strict for internal, liberal for public API rustdoc). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-select Extends transfer() / auto_select_inputs to accept [ReduceOutput(0)] in addition to [DeductFromInput(0)]. Output 0 absorbs the fee, so input selection skips the fee-headroom reservation. Σ inputs == Σ outputs invariant preserved via last- input trim. 5 new tests in auto_select_tests cover happy path, multi-input trim, multi- output isolation, output-too-small error, and structural validation. Resolves PR #3549 thread r-aCky's production prerequisite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…om key_wallet Replace the duplicated DEFAULT_ACCOUNT_INDEX / DEFAULT_KEY_CLASS constants with a default_platform_payment_account_key() helper that destructures key_wallet's PlatformPaymentAccountSpec::default(), and pin the const _PUB values to the same canonical struct's fields. A colocated drift test asserts PlatformPaymentAccountSpec::default() still matches our pinned constants — preventing silent drift if upstream defaults change. WalletAccountCreationOptions::Default is a unit variant (the (account, key_class) shape lives in the BTreeSet variant, not Default itself), so destructuring Default directly was not viable. Pinning to PlatformPaymentAccountSpec — the canonical "what does Default mean for a PlatformPayment account" struct — is the closest equivalent. Resolves PR #3549 thread r-aA6u. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t(0) Pay fees by reducing output 0 instead of deducting from input 0. Simpler to reason about for test authors — recipients see (requested - fee_share), no input-side reservation needed. KNOWN BREAKAGE: the existing transfer_between_two_platform_addresses test asserts an exact recipient balance and will fail under the new default (recipient receives 10M - fee_share). Test fixture update is a follow-up. Also re-aligns import ordering in this file with `cargo fmt --all` defaults (a minor stray drift from the previous commit). Resolves PR #3549 thread r-aCky. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dex 0 Sweep-back target uses the bank's address-0 deterministically instead of advancing the unused-address pool every test run. Avoids accumulation of empty addresses on the bank wallet across test invocations. Implementation: derive the DIP-17 platform-payment address at index 0 directly from the bank seed (mirroring simple-signer's derivation logic), side-stepping the AddressPool's "next unused" cursor that would skip index 0 once it gets marked used. Resolves PR #3549 thread r-Jhi_. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…put-from-output Sweep-to-bank uses ReduceOutput(0) so the bank absorbs the fee from its incoming sum. Drops SWEEP_FEE_ESTIMATE constant and the multi-input fee headroom math. Sweep gate is now "if address balance > 0". Resolves PR #3549 thread r-ZluD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…noop stubs Split drain_to_bank into per-source helpers: sweep_platform_addresses (active), sweep_identities, sweep_core_addresses, sweep_unused_core_asset_locks, sweep_shielded (all noop with TODOs). teardown_one and sweep_orphans now walk every source type so future sweep implementations slot in cleanly. Resolves PR #3549 thread r-Zoq9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-platform-wallet-e2e
…tication_path
`AccountType::IdentityAuthenticationEcdsa { identity_index }` was
removed from `key-wallet` between revs `4c8bec3` and `ea33cbc8`.
Replaced with the new top-level `DerivationPath::identity_authentication_path(
network, KeyDerivationType::ECDSA, identity_index, key_index)` API
(`key-wallet/src/bip32.rs:1115`), which bakes both identity_index and
key_index into the path directly — `key_index` becomes the loop
variable instead of an external `extend([leaf])` step.
…ectly PR #3549 dedup re-audit (PROJ-001): the local DEFAULT_GAP_LIMIT = 20 shadows key_wallet's canonical pub const DIP17_GAP_LIMIT (rust-dashcore ea33cbc8: key-wallet/src/gap_limit.rs:26). Drop the local constant and import the upstream one — drift here would silently de-sync the harness from key-wallet's own gap policy.
…gh PlatformWallet PR #3549 dedup re-audit (PROJ-002): derive_platform_address_at_index was running BIP-32 manually from raw seed bytes. The bank already holds a PlatformWallet whose Wallet::derive_public_key (key-wallet/src/wallet/ helper.rs:763) does the same thing, so the parallel-derivation surface was redundant. Take the existing &Arc<PlatformWallet>, call .state().await.wallet().derive_public_key(&path), hash the result. Drops the bip39 seed-bytes consumer (the seed bytes are still derived once for SeedBackedPlatformAddressSigner::new four lines below). Net removes RootExtendedPrivKey, Secp256k1, PublicKey imports from bank.rs.
…r wrapper `framework/signer.rs` was a 78-line do-nothing shell around `SimpleSigner::from_seed_for_platform_address_account`: - the `Signer<PlatformAddress>` trait impl just delegated to inner; - `SimpleSigner` already implements that trait directly (`packages/simple-signer/src/signer.rs:338`); - `cached_key_count` and `new_with_gap` had zero callers outside the module; - the only added value was pinning `account=0`/`key_class=0`, which collapses to four lines of construction code. Replace with `framework::make_platform_signer(seed_bytes, network) -> SimpleSigner` next to the `FrameworkError`/`FrameworkResult` types in `mod.rs`. The three call sites (`bank.rs`, `wallet_factory.rs`, `cleanup.rs`) now hold `SimpleSigner` directly and pass it straight to `PlatformAddressWallet::transfer`. `TestWallet::address_signer()` returns `&SimpleSigner` for the same reason.
|
@coderabbitai review all |
…-dev' into feat/rs-platform-wallet-e2e
… existing FFI code Core split PlatformWalletError::NoSelectableInputs into three typed variants (NoSpendableInputs, OnlyOutputAddressesFunded, OnlyDustInputs). The FFI matcher still referenced the old variant name and failed to compile. Widen the dedicated ErrorNoSelectableInputs (=14) mapping to cover all three new variants so Swift consumers keep the same numeric code and the typed Display rendering survives as the message. The FFI ABI is preserved — no renumber, no new code, just a broadened match arm and refreshed docs/test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…OnlyOutputAddressesFunded The is_nonce_class_error_rejects_no_selectable_inputs test still constructed the now-deleted PlatformWalletError::NoSelectableInputs variant. That variant was split into NoSpendableInputs / OnlyOutputAddressesFunded / OnlyDustInputs (see commit dc5e611 which fixed the FFI layer for the same rename). The test target failed to compile (E0599), blocking the whole --test e2e suite from running. Retarget the assertion onto OnlyOutputAddressesFunded — the field shape is the cleanest match to the old NoSelectableInputs{funded_outputs} and preserves the original semantic intent: an insufficient-funds-shape error must NOT be classified as nonce-class (retrying would just churn against the same empty pool). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…b, fix spec drift, file Found-026, link Found-006 to rust-dashcore#762 PA-003 (`green` → `red-real-fail (test-bug)`): marker pre-funding pollutes `address_funds` rows so the 5-output transfer pays cheap UPDATE per recipient while the 1-output transfer pays the one-time CREATE. Observed Δfee ≈ 536k matches one absent create. Drive's chain-time fee at `validate_fees_of_event/v0/mod.rs:195` drives the cost off real drive ops, not the static `state_transition_min_fees` floor. The line-235 invariant is misformulated for the chosen address-derivation strategy. PA-005b spec drift resolved → truth is `blocked`. Both prior `PASS` claims (detailed body at line ~534 and the "PR #3609 merged" changelog entry) were stale: they landed on 2026-05-11 in commit `5c6baabd8f` without re-running against the QA-002 setup hook that had landed seven days earlier on 2026-05-04 (commit `94902be73b`). Three-way contract mismatch: QA-002's `consume_platform_address_index_zero` marks index 0 used while the DIP-17 pool eagerly generates indices `0..=19` in `AddressPool::new`, and the headroom helper at `framework/gap_limit.rs:188-207` measures fresh-past-`highest_generated` rather than any-unused-below-ceiling. PA-008b (`green` → `red-real-fail (concurrency-only)`): full-suite 14-thread cohort FAILS at the canonical 120s `wait_for_balance` timeout on the first marker funding; `--test-threads=1` isolation re-run PASSES in 158s. Suspected race in `PlatformAddressWallet::next_unused_receive_address` (`platform_addresses/wallet.rs:223-270`) vs concurrent BLAST syncs from sibling tests. Found-026 added (P2, MEDIUM, suspected) — `next_unused_receive_address` pool-cursor bump may not enqueue the address into the BLAST sync provider's pending set under concurrent load. Symmetric to Found-025 on the rs-sdk side. Found-006 upstream issue filed: **dashpay/rust-dashcore#762** — Add `top_up_index` field to `CreditOutputFunding::IdentityTopUp` (DIP-9 conformance gap). Wallet-side TODO in `top_up.rs` updated to reference the issue; once it lands, drop the `_` prefix on `topup_index` and forward it through the derivation path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sues, drop stale wording, sync N=3
… amplifier note Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up dashpay/rust-dashcore#756 which adds chainlock-driven transaction finalization in the wallet layer. Previously, `WalletInterface` had no `process_chain_lock` method and `dash-spv`'s `SyncEvent::ChainLockReceived` was emitted but never consumed, so wallet records were stuck at `TransactionContext:: InBlock(_)` forever even when the network produced a chainlock for the containing block. The new pin promotes records `InBlock → InChainLockedBlock` on chainlock arrival and emits a new `WalletEvent::TransactionsChainlocked` variant carrying the chainlock proof and per-account net-new finalized txids. For our `wait_for_proof` poll loop this means the chainlock branch (`record.context.is_chain_locked()`) actually flips when peers deliver the chainlock — the iter-4 IS→CL fallback path now resolves correctly instead of timing out at the secondary 180 s deadline. The new `WalletEvent` variant forces match-arm coverage in two sites: - packages/rs-platform-wallet/src/changeset/core_bridge.rs `build_core_changeset` returns `CoreChangeSet::default()` for the new variant. The wallet has already mutated the in-memory record by the time the event fires (upstream is "mutate-then- emit"), and the poll loop reads `record.context.is_chain_locked()` directly, so no additional persister projection is needed today. A future enhancement could persist `WalletMetadata:: last_applied_chain_lock` for crash recovery, but that's out of scope here. - packages/rs-platform-wallet/src/wallet/core/balance_handler.rs `BalanceUpdateHandler::on_wallet_event` returns early for the new variant. Chainlocks promote finality (`InBlock → InChainLockedBlock`) without changing UTXO state, so there's no balance update to deliver. Extracted from PR #3634 commit 4184a42 onto feat/rs-platform-wallet-e2e. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 546 (QA-901) TRACE re-investigation 2026-05-14 confirmed the earlier deterministic failure was a test-side dust-threshold mismatch (test assumed 2,730 duffs; upstream `transaction_builder.rs:294`, rev `5313086…`, uses 546). Headroom changed from 2,500 → 700; new change range [200, 474] is fully sub-dust across the observed [226, 500] testnet fee range, so the builder folds it into the fee and the BIP-32 account is truly drained. CR-004 reclassified red-by-design (dash-evo-tool#845) → passing-as-regression. The test now pins the symmetric BIP-32 spent-marking path (TransactionRouter → ManagedAccountCollection → check_transaction_for_match → update_utxos) plus the upstream sub-dust fold contract. The dash-evo-tool#845 reference is retained as a historical footnote — the symmetric spent-marking path was confirmed working; any remaining DET surface lives in dash-evo-tool's own UI refresh path, outside this suite's contract. TEST_SPEC.md updates: matrix row, detailed body (Layer 2 description + bug repro note), changelog entry, post-v47 status section, and counts annotation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-link the newly-filed #3642 from the 5 hard-coded BIP-44 lookup sites in proof.rs + recovery.rs (TODO comments, no logic change) and from TEST_SPEC.md (matrix rows + detail front-matter + changelog entry). Found-012 and Found-023 unify on the same downstream-only fix: replace `info.core_wallet.accounts.standard_bip44_accounts.get(&account_index)` with iteration over `all_funding_accounts()` — no upstream change required; SPV-side tracking already covers all account types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(already fixed) Both pins describe contracts that are already satisfied in HEAD: - Found-019 (SeedBackedIdentitySigner ECDSA_HASH160 re-hash) — fix landed at packages/rs-platform-wallet/tests/e2e/framework/signer.rs:148-154 in commit 59cba08 (PR #3563). identity_key_lookup branches on key_type; ECDSA_HASH160 uses key.data() as-is, no re-hash. Production packages/simple-signer/src/signer.rs does NOT have the bug shape (different storage models). - Found-020 (PA-001b output_change_address spec/impl drift) resolved via spec realignment in PR #3609 (option a). PA-001b rewritten to match implicit-change semantics. The parameter doesn't exist in production and isn't planned. Knowledge preserved in memcan; spec clutter dropped. Total Found-bug pins 26 → 24. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…routing proof CR-004 now pins the dash-evo-tool#845 contract directly: a BIP-32 send leaves an above-dust change UTXO, post-broadcast check_core_transaction routes it back onto the BIP-32 account (count == 1), and a follow-up spend consumes exactly that routed-back change and succeeds — proving the change was tracked back into BIP-32, not orphaned. Module doc states only the present contract (history narration dropped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e fee with symmetric pre-markers PA-003 measures the real chain-time fee (Σ gross outputs − Σ destination balance deltas, the canonical Σ inputs − Σ outputs under [ReduceOutput(0)]) for two self-transfers that draw inputs exclusively from a single source address (InputSelection::Explicit). Every measured destination — including the 1-output dest_1 — is pre-markered so both shapes hit address-funds UPDATE storage ops with no one-off CREATE skew; output count is the sole varied factor. Restored guards: strict fee_5 > fee_1, sub-linear fee_5 < fee_1*5, and the explicit FEE_DELTA_CEILING linear-fee-schedule tripwire. Funding defect fix: the explicit-input map value is the actual input amount the transition encodes (it must balance Σ outputs and be backed by the address balance), not a placeholder weight. inputs_1 now uses OUTPUT_AMOUNT and inputs_5 uses 5×OUTPUT_AMOUNT. addr_src funding bumped 500M → 700M to cover six MARKER_AMOUNT pre-markers (180M) plus both measured transfers (50M + 250M) with headroom, so addr_src always holds ≥ the explicit input amount when each transition is built. TEST_SPEC.md: PA-003 status flipped to green (table row + body) with a concise changelog line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…real eager-pool state Production's DIP-17 platform-payment pool is built by the eager AddressPool::new, which fills indices 0..=gap_limit-1 at construction (highest_generated = Some(gap_limit-1)); the QA-002 setup hook then marks index 0 used. From that real state the batch helper's fresh-past-highest_generated headroom is highest_used + 1 = 1, so the triplet's empty-pool full-window premise is unreachable in production. Rather than suppressing eager fill or changing the (correct) shared helper math at framework/gap_limit.rs, the test now models a real wallet that has cycled its first DIP-17 gap window: a test-scoped open_full_gap_window marks index gap_limit-1 used, shifting the ceiling up by gap_limit to open a genuine gap_limit-wide fresh window. The same DIP-17 boundary triad is pinned from the production starting state: - A: request gap_limit-1 fresh addresses, assert success + all distinct - B: request gap_limit (boundary), assert success + all distinct - C: request gap_limit+1, assert GapLimitError::Exceeded with every field pinned against the LIVE post-mark watermarks (requested, available=gap_limit, gap_limit, highest_used=Some(gap_limit-1), highest_generated=Some(gap_limit-1)), then a boundary retry proves the rejection did not mutate the pool Shared helper semantics are unchanged. TEST_SPEC.md PA-005b updated in all three places (quick-index, body Status, changelog) to the accurate rebaselined contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ead-back (QA-014)
PA-009 sub-case C's post-teardown observation re-derived the gone test
wallet and trusted its recent-zone sync watermark. A watermark-less
re-derived wallet's sync_balances(AddressSyncConfig{
full_rescan_after_time_s: 0 }) resolves to a recent-zone-only query
that returned 0 for addr_1 even though the sub-min_input dust was
correctly abandoned and never swept — a non-deterministic harness gap
(QA-014), not a production defect. The v53 14-thread run failed at the
re-derive read-back assertion (pa_009_min_input_amount.rs:290,
left: 0, right: 1000) while the cleanup-gate abandon-dust path itself
worked correctly.
Replace the re-derive read-back with a direct proof-verified on-chain
read of addr_1 via wait_for_address_balance_chain_confirmed — the same
AddressInfo::fetch gate the funding step already uses successfully
earlier in the same test. The post-condition now asserts addr_1 still
holds exactly TARGET_RESIDUAL on chain, reading real chain state
instead of a stale re-derived local view.
All three pinned invariants are preserved and strengthened:
(a) below-gate dust abandoned, no sweep transition broadcast (a swept
dust drops the balance to ~0, timing out the gate);
(b) gate == PlatformVersion::latest().dpp.state_transitions.address_funds
.min_input_amount and is positive (sub-cases A/B, untouched);
(c) addr_1 residual remains on chain at exactly TARGET_RESIDUAL.
#[ignore] and #[tokio_shared_rt::test(shared)] retained (network-gated,
the standard for all on-chain e2e cases; suite runs --include-ignored).
TEST_SPEC.md PA-009 references (quick-index, body Status/Scenario,
changelog) updated consistently; no stale QA-014/degenerate drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Found-025 sync-discard race TK-001 and TK-014 failed in the v53 14-thread run, both timing out in the SETUP FUNDING gate before any token logic ran (tk_001_token_transfer.rs:67 setup_with_token_and_two_identities; tk_014_token_group_action.rs:109 setup_with_per_identity_funding). In both, bank.fund_address chain-confirmed the funding (nonce streak 2/2) before the wait, then the rs-sdk address-sync silently discarded the fetched balance update because the target address was not yet in pending_addresses — Found-025, amplified by 14-thread concurrency. Not production defects: transfer/group-action/co-sign code never executed and siblings (TK-001b/c, TK-009/010/012) were green in the same run. Root cause in the shared chokepoint framework/mod.rs::setup_with_per_identity_funding: it gated on wait_for_balance, whose proof-verified hand-off only runs AFTER the Found-025-poisoned local sync map (balances().get(addr)) first reaches target — so under Found-025 the proof gate was never reached and the budget expired in the local-view branch (60-62 polls, no chain-confirmed line). Fix: observe funding directly via the proof-verified AddressInfo::fetch path (wait_for_address_balance_chain_confirmed_n, CHAIN_CONFIRMED_CONSECUTIVE_SUCCESSES) — the same chain-state read the validator walks and the family PA-009c adopted — bypassing the poisoned map entirely. The existing strong wait_for_address_known_to_platform gate is unchanged. Only the funding-observation mechanism changed: no funding amounts, identity counts, contract publish, propose/co-sign, or token/identity assertions altered. Deterministic and concurrency-independent, so it hardens the whole setup-helper blast radius (all 22 TK-*/ID-*/CR-003/DPNS-001 cases routing through setup_with_per_identity_funding). No new Found-NNN pin and no upstream issue (Found-025 already owns the root cause). A TK-wave serialization / worker-pool cap remains a documented fallback only — not implemented, since the proof-verified read-back structurally bypasses the poisoned map. TEST_SPEC.md: TK-001 (quick-index + body) and TK-014 (quick-index + body) reclassified green -> red-real-fail mirroring TK-007 wording, cross-linked to Found-025; one changelog entry added. All three references per test are mutually consistent (no stale green/PASS-in-v47 drift). Live e2e requires a bank-funded node (yarn start) unavailable in this environment; verified by inspection + cargo build --tests + cargo clippy (both clean). Live re-validation deferred to the combined v54 run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ound-008 env-mask FIX 1 (#475): the e2e README documented the opt-in invocation with `--ignored`, which runs ONLY the `#[ignore]`-attributed subset (~40/108) and silently skips owned-fix cases. Corrected to `--include-ignored` so the full suite runs, with a one-line note explaining why `--ignored` alone is wrong. FIX 2 (#474, CLAUDE.md infra-blocker rule): added a `// TODO(env):` marker in al_001 near the Core-funded setup gate and a brief note in TEST_SPEC.md's AL-001/Found-008 entry recording that the Found-008 pin is env-masked when the e2e testnet Core L1 bank is depleted (al_001 dies at the setup gate, not at the designed FinalityTimeout; same depletion also fails cr_003 + id_002b). Funding address phrased generically — the specific address is being verified separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…of-band) User will top up the e2e Core L1 bank wallet directly, so the env-mask documentation is unnecessary. Removes the FIX 2 content added in the previous commit: the `// TODO(env):` marker in al_001 near the Core-funded setup gate and the AL-001/Found-008 "pin coverage degraded under Core-bank depletion" bullet in TEST_SPEC.md. Net effect of this commit pair is README-only — the `--ignored` -> `--include-ignored` run-flag correction (#475). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ter store-error wallet loss) register_wallet logs and swallows the registration-round persister `store` error (manager/wallet_lifecycle.rs:276-282) then inserts the wallet into self.wallets unconditionally (wallet_lifecycle.rs:347-349). A successful-looking import leaves no persisted record and vanishes on the next launch — HIGH-severity silent data loss. Note the asymmetry: the load_persisted / initialize_from_persisted failure paths in the same function already roll back and return Err; the registration store does not. Deterministic pin (no live network, no concurrency): injects a StoreFailsPersister whose `store` returns Err while `load`/`flush` succeed (so the already-correct load_persisted rollback path does not mask the defect), drives create_wallet_from_seed_bytes through a mock-SDK manager, and asserts the correct atomic-failure contract — the call returns Err AND the wallet is absent from wallet_ids(). Fails today for the real reason; flips green once the registration store is treated as load-bearing. #[ignore]d — live run deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue being fixed or feature implemented
End-to-end test framework and scenario suite for
rs-platform-wallet, plus a campaign of e2e-fix commits that pin known upstream behaviors and harden setup gates. Validated by the v54 run: 98 PASS / 10 FAIL — the 10 failures are upstream red-by-design pins plus env-masked cases, not regressions.What was done?
e2e framework + scenarios live here. This update lands the e2e-fix campaign:
63ee3ba443): retarget to the positive feat(wasm-dpp): provide external entropy generator to document factory #845 change-routing proof — BIP-32 UTXO routing pin after spend.d3c02ca20b): pin fee scaling on real chain-time fee with symmetric pre-markers.7651ca8d6a): rebaseline DIP-17 gap-limit triplet to real eager-pool state.f625f830ee): pin via deterministic on-chain read-back (QA-014).f78dedad53): harden setup gates against Found-025 sync-discard race via proof-verifiedwait_for_address_balance_chain_confirmed_n.f5ddda61c0, docsc6693df4f2): correct run flag (--include-ignored) and flag al_001 Found-008 env-mask.Production-code carve-out posture: this branch carries production clusters that have their own dedicated PRs and are NOT owned here:
The e2e framework and scenarios remain in this PR.
How Has This Been Tested?
Full e2e run v54: 98 PASS / 10 FAIL. The 10 failures are accounted for as upstream red-by-design pins plus env-masked cases — no functional regressions introduced by this campaign.
Breaking Changes
None. Test-only and documentation changes; no production API surface modified by this PR.
Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com